<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="coinfaces">

        <title>JavaScript二进制数据处理</title>

    <link rel="stylesheet" href="../../theme/css/style.css">
    <link rel="stylesheet" href="../../theme/css/pygments.css">



</head>

<body>
    <div class="content">
<h1>JavaScript二进制数据处理</h1>    <p>
        under
    </p>
    <p>
        in <a href="../../categories/tech/">tech</a>
    </p>
    <p>Published: 2020-02-28</p>


    <h3>JavaScript类型化数组</h3>
<p>使用JavaScript类型化数组可以方便地操作二进制数据，它分为两层，数据缓存和数据视图，数据缓存（<code>ArrayBuffer</code>）用于储存数据，但不负责数据读写，读写数据需要通过创建数据视图来实现，类型化数组就是数据视图的一种。</p>
<p>数据缓存和数据视图的关系如下：</p>
<div class="highlight"><pre><span></span><code><span class="kd">let</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ArrayBuffer</span><span class="p">(</span><span class="mi">16</span><span class="p">);</span>
<span class="nx">buffer</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="c1">// 16</span>
<span class="kd">let</span> <span class="nx">int32View</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Int32Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
<span class="nx">int32View</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="c1">// 4</span>
<span class="kd">let</span> <span class="nx">int16View</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Int16Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
<span class="nx">int16View</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="c1">// 8</span>
</code></pre></div>


<p>很多操作二进制数据的API用到了类型化数组</p>
<ul>
<li>文件操作：<code>FileReader.prototype.readAsArrayBuffer()</code></li>
<li>AJAX：<code>XMLHttpRequest.prototype.send()</code></li>
<li>图像操作：<code>ImageData.data</code></li>
</ul>
<h3>File和Blob</h3>
<p><code>File</code>和<code>Blob</code>提供了文件操作的接口，文件也可以看作是一块（尚未加载的）二进制数据，需要读出来，因此其操作大多是异步的。<code>File</code>和<code>Blob</code>的区别在于后者提供了文件基本操作，前者在此基础上增加文件的常用属性，如<code>name</code>、<code>lastModified</code>。</p>
<p>读文件内容的通用方法是创建一个<code>FileReader</code>对象，它提供了将文件读取为<code>ArrayBuffer</code>、dataURL、文本或者Binary String的API。</p>
<p>比较简单的应用可以使用<code>Blob.arrayBuffer()</code>的内置方法读取二进制数据，它返回一个<code>Promise</code>，不提供读取进度的反馈信息。UTF8编码的文本文件可以使用<code>Blob.text()</code>读取其内容，如果文本文件不是UTF8编码，则需要使用<code>FileReader</code>。</p>
<p><code>URL.createObjectURL()</code>可以将文件内容转化为dataURL，这是同步操作，不适用于大文件或者网络带宽比较小的场景。</p>
<h3>String</h3>
<p>String类型不适合处理二进制数据，因为String是文本的抽象，即Unicode字符集上元素的集合。文本和适合持久存储的二进制数据之间存在编解码过程，所有文本都可以通过编码转化成二进制数据，但反过来不成立，即对于任一编码，都存在无法解码成文本的二进制数据，因此String类型不适合处理二进制数据。</p>
<p>虽然任何文本编码都不是String和二进制数据之间的双向映射，但可以创建一个这样的映射，因为JavaScript String是用UTF16字符实现的，每个字符理论上可以表示0-65535之间的任意值，而二进制数据的每个字节都可以用0-255之间的任意数来表示，因此String的每个字符和二进制数据的每个字节之间可以建立联系：<code>s.charCodeAt(i) === new Uint8Array(buffer)[i]</code>，这就是Binary String，一种与文本无关、专为处理二进制数据而存在的数据类型。</p>
<p>Binary String用了16位来表示8位有效信息，虽然浪费了一倍的空间，但避免了不少问题，例如字节序、二进制数据字节长度为单数、UTF-16代理对（surrogate pair）。</p>
<h3>Base64</h3>
<p>Base64（<a href="https://tools.ietf.org/html/rfc4648#section-4">RFC 4648</a>）是一种用ASCII字符表示二进制数据的编码格式，共使用了64个可打印的ASCII字符，每个字符可表示6位有效信息，每3个字节的数据被编码成4个ASCII字符，数据大小增加了约1/3。</p>
<p>JavaScript提供了<code>btoa()</code>将Binary String编码为base64，以及<code>atob()</code>将base64解码为Binary String。如果文本只包含ASCII字符，也可以直接把它交给<code>btoa()</code>来编码，因为这种String数据和Binary String恰好是相同的。超出这个范围，天然的一对一关系就不存在了，必须把文本先编码成二进制数据，再将二进制数据编码成base64，这种情况下要确保编码方和解码方使用的字符编码相同，UTF16（效率高）或UTF8（通用）是常见的选择，<a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem">MDN</a>提出将String编码成base64的多种方案，都是基于UTF16或者UTF8（使用<code>encodeURIComponent()</code>的例子可以认为是基于UTF8）。</p>
<h3>dataURI</h3>
<p>JavaScript提供了<code>encodeURI()</code>和<code>encodeURIComponent()</code>两个函数将String编码成UTF8，但将大部分字节（UTF8每个字符可能用1、2、3、4个字节来表示）进行了转义。这两个函数的区别是，前者不会对URI里的保留字符如<code>/</code>、<code>:</code>、<code>@</code>、<code>?</code>、<code>&amp;</code>、<code>#</code>进行转义，其输出可以直接作为URI使用，后者会对URI里的特殊字符进行转译，其输出可以嵌入到URI的内容里。</p>
<p>这两个函数可以对所有有效的文本进行编码（如果String数据中存在不完整的UTF16代理对，会抛出异常：<code>URIError: malformed URI sequence</code>），对于二进制数据，可以先将其转化为base64再编码成URI，因此所有数据都可以用URI来表示，其标准化的描述就是dataURI（<a href="https://tools.ietf.org/html/rfc2397">RFC 2397</a>）。</p>
<p>dataURL的格式如下：</p>
<div class="highlight"><pre><span></span><code><span class="c">data:[&lt;mediatype&gt;][;base64],&lt;data&gt;</span>
</code></pre></div>


<p>mediatype包括数据的MIME类型和字符编码等其它信息，base64表示使用了base64编码，data部分是经过URI转义的数据。尽管创建一个dataURL不难（前面已经叙述），读取dataURL则可能遇到很多复杂的情况（mediatype）。</p>
<div class="highlight"><pre><span></span><code><span class="err">mediatype  := [ type &quot;/&quot; subtype ] *( &quot;;&quot; parameter )</span>
<span class="err">parameter  := attribute &quot;=&quot; value</span>
</code></pre></div>


<p>目前看到的最靠谱的方式是用<a href="https://stackoverflow.com/a/30666203/9634290">XHR</a>：</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span> <span class="nx">dataURLtoBlob</span><span class="p">(</span><span class="nx">dataUrl</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">req</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">;</span>

    <span class="nx">req</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span> <span class="s1">&#39;GET&#39;</span><span class="p">,</span> <span class="nx">dataUrl</span> <span class="p">);</span>
    <span class="nx">req</span><span class="p">.</span><span class="nx">responseType</span> <span class="o">=</span> <span class="s1">&#39;arraybuffer&#39;</span><span class="p">;</span> <span class="c1">// Can&#39;t use blob directly because of https://crbug.com/412752</span>

    <span class="nx">req</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span> <span class="nx">fileLoaded</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// If you require the blob to have correct mime type</span>
        <span class="kd">var</span> <span class="nx">mime</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getResponseHeader</span><span class="p">(</span><span class="s1">&#39;content-type&#39;</span><span class="p">);</span>

        <span class="nx">callback</span><span class="p">(</span><span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="k">this</span><span class="p">.</span><span class="nx">response</span><span class="p">],</span> <span class="p">{</span><span class="nx">type</span><span class="o">:</span> <span class="nx">mime</span><span class="p">}));</span>
    <span class="p">};</span>

    <span class="nx">req</span><span class="p">.</span><span class="nx">send</span><span class="p">();</span>
<span class="p">}</span>

<span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span>

<span class="nx">dataURLtoBlob</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span> <span class="nx">blob</span> <span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span> <span class="nx">blob</span> <span class="p">);</span>
<span class="p">});</span>
</code></pre></div>


<p>或者<a href="https://stackoverflow.com/a/36183379/9634290"><code>fetch</code> API</a>：</p>
<div class="highlight"><pre><span></span><code><span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span>

<span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="p">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">blob</span><span class="p">())</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">blob</span> <span class="p">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">blob</span><span class="p">))</span>
</code></pre></div>


<h3>参考</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays">MDN - JavaScript类型化数组</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding">MDN - Base64编码和解码</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications">MDN - 在Web应用中使用文件</a></li>
</ul>
    </div>
<nav class="nav sidebar">
    <div class="siteinfo">
        <div class="sitename"><a href="../..">coinfaces</a></div>
        <div class="tagline">Another Side of Life</div>
    </div>

        <a href="../../categories/">categories</a>
        <a href="../..">&larr; Home</a>
</nav><footer class="footer">
    <span>
        &copy; jpli &middot;
        under <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA 4.0</a>
    </span>
    |
    <span>
        Powered by:
        <a href="http://blog.getpelican.com/">Pelican</a>
    </span>
</footer></body>
</html>